/*
 * Created on May 12, 2010
 * Created by Paul Gardner
 * 
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package org.gudy.azureus2.core3.download.impl;

import java.util.HashMap;
import java.util.Map;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.peer.PEPeerManagerStats;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.DisplayFormatters;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TimerEventPeriodic;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.networkmanager.LimitedRateGroup;
import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.speedmanager.SpeedManager;
import com.aelitis.azureus.core.speedmanager.SpeedManagerLimitEstimate;
import com.aelitis.azureus.core.speedmanager.SpeedManagerPingMapper;

public class DownloadManagerRateController {
    private static AzureusCore core;
    private static SpeedManager speed_manager;

    private static Map<PEPeerManager, PMState> pm_map = new HashMap<PEPeerManager, PMState>();

    private static TimerEventPeriodic timer;

    private static AsyncDispatcher dispatcher = new AsyncDispatcher("DMCRateController");

    private static boolean enable;
    private static boolean enable_limit_handling;
    private static int slack_bytes_per_sec;

    static {
        COConfigurationManager.addAndFireParameterListeners(new String[] { "Bias Upload Enable", "Bias Upload Handle No Limit",
                "Bias Upload Slack KBs", }, new ParameterListener() {
            public void parameterChanged(String parameterName) {
                enable = COConfigurationManager.getBooleanParameter("Bias Upload Enable");
                enable_limit_handling = COConfigurationManager.getBooleanParameter("Bias Upload Handle No Limit") && enable;
                slack_bytes_per_sec = COConfigurationManager.getIntParameter("Bias Upload Slack KBs") * 1024;
            }
        });
    }

    private static volatile int rate_limit = 0;

    private static LimitedRateGroup limiter = new LimitedRateGroup() {
        public String getName() {
            return ("DMRC");
        }

        public int getRateLimitBytesPerSecond() {
            return (rate_limit);
        }

        public void updateBytesUsed(int used) {
        }
    };

    private static final int TIMER_MILLIS = 1000;

    private static final int WAIT_AFTER_CHOKE_PERIOD = 10 * 1000;
    private static final int WAIT_AFTER_CHOKE_TICKS = WAIT_AFTER_CHOKE_PERIOD / TIMER_MILLIS;

    private static final int DEFAULT_UP_LIMIT = 250 * 1024;
    private static final int MAX_UP_DIFF = 15 * 1024;
    private static final int MAX_DOWN_DIFF = 10 * 1024;
    private static final int MIN_DIFF = 2 * 1024;

    private static final int SAMPLE_COUNT = 5;
    private static int sample_num;
    private static double incomplete_samples;
    private static double complete_samples;

    private static int ticks_to_sample_start;

    private static int last_rate_limit;
    private static double last_incomplete_average;
    private static double last_complete_average;
    private static double last_overall_average;

    private static int tick_count = 0;
    private static int last_tick_processed = -1;

    private static long pm_last_bad_limit;
    private static int latest_choke;
    private static int wait_until_tick;

    public static String getString() {
        if (enable) {

            String str = "reserved=" + DisplayFormatters.formatByteCountToKiBEtcPerSec(slack_bytes_per_sec);

            if (enable_limit_handling) {

                str += ", limit=" + DisplayFormatters.formatByteCountToKiBEtcPerSec(rate_limit);

                str +=
                        ", last[choke=" + DisplayFormatters.formatByteCountToKiBEtcPerSec(latest_choke) + ", ratio="
                                + DisplayFormatters.formatDecimal(last_incomplete_average / last_complete_average, 2) + "]";

                return (str);

            } else {

                return (str);
            }
        } else {

            return ("Disabled");
        }
    }

    public static void addPeerManager(final PEPeerManager pm) {
        dispatcher.dispatch(new AERunnable() {
            public void runSupport() {
                if (core == null) {

                    core = AzureusCoreFactory.getSingleton();

                    speed_manager = core.getSpeedManager();
                }

                boolean is_complete = !pm.hasDownloadablePiece();

                PEPeerManagerStats pm_stats = pm.getStats();

                long up_bytes = pm_stats.getTotalDataBytesSentNoLan() + pm_stats.getTotalProtocolBytesSentNoLan();

                if (is_complete) {

                    pm.addRateLimiter(limiter, true);
                }

                pm_map.put(pm, new PMState(pm, is_complete, up_bytes));

                if (timer == null) {

                    timer = SimpleTimer.addPeriodicEvent("DMRC", TIMER_MILLIS, new TimerEventPerformer() {
                        public void perform(TimerEvent event) {
                            dispatcher.dispatch(new AERunnable() {
                                public void runSupport() {
                                    update();
                                }
                            });
                        }
                    });
                }
            }
        });
    }

    public static void removePeerManager(final PEPeerManager pm) {
        dispatcher.dispatch(new AERunnable() {
            public void runSupport() {
                pm_map.remove(pm);

                if (pm_map.size() == 0) {

                    timer.cancel();

                    timer = null;

                    rate_limit = 0;
                }
            }
        });
    }

    private static void update() {
        tick_count++;

        if ((!enable_limit_handling) || pm_map.size() == 0 || NetworkManager.isSeedingOnlyUploadRate()
                || NetworkManager.getMaxUploadRateBPSNormal() != 0 || core == null || speed_manager == null
                || speed_manager.getSpeedTester() == null) {

            rate_limit = 0;

            return;
        }

        int num_complete = 0;
        int num_incomplete = 0;

        int num_interesting = 0;

        int i_up_total = 0;
        int c_up_total = 0;

        long mono_now = SystemTime.getMonotonousTime();

        for (Map.Entry<PEPeerManager, PMState> entry : pm_map.entrySet()) {

            PEPeerManager pm = entry.getKey();
            PMState state = entry.getValue();

            boolean is_complete = !pm.hasDownloadablePiece();

            PEPeerManagerStats pm_stats = pm.getStats();

            long up_bytes = pm_stats.getTotalDataBytesSentNoLan() + pm_stats.getTotalProtocolBytesSentNoLan();

            long diff = state.setBytesUp(up_bytes);

            if (is_complete) {

                num_complete++;

                c_up_total += diff;

            } else {

                num_incomplete++;

                i_up_total += diff;

                if (state.isInteresting(mono_now)) {

                    num_interesting++;
                }
            }

            if (state.isComplete() != is_complete) {

                if (is_complete) {

                    pm.addRateLimiter(limiter, true);

                } else {

                    pm.removeRateLimiter(limiter, true);
                }

                state.setComplete(is_complete);
            }
        }

        if (num_incomplete == 0 || num_complete == 0 || num_interesting == 0) {

            rate_limit = 0;

            return;
        }

        boolean skipped_tick = false;

        if (last_tick_processed != tick_count - 1) {

            pm_last_bad_limit = 0;
            latest_choke = 0;
            wait_until_tick = 0;

            ticks_to_sample_start = 0;
            sample_num = 0;
            incomplete_samples = 0;
            complete_samples = 0;

            skipped_tick = true;
        }

        last_tick_processed = tick_count;

        if (skipped_tick || tick_count < wait_until_tick) {

            return;
        }

        try {
            long real_now = SystemTime.getCurrentTime();

            SpeedManagerPingMapper mapper = speed_manager.getActiveMapper();

            if (rate_limit == 0) {

                rate_limit = speed_manager.getEstimatedUploadCapacityBytesPerSec().getBytesPerSec();

                if (rate_limit == 0) {

                    rate_limit = DEFAULT_UP_LIMIT;
                }
            }

            SpeedManagerLimitEstimate last_bad = mapper.getLastBadUploadLimit();

            if (last_bad != null) {

                int last_bad_limit = last_bad.getBytesPerSec();

                if (last_bad_limit != pm_last_bad_limit) {

                    pm_last_bad_limit = last_bad_limit;

                    SpeedManagerLimitEstimate[] bad_ups = mapper.getBadUploadHistory();

                    int total = last_bad.getBytesPerSec();
                    int count = 1;

                    for (SpeedManagerLimitEstimate bad : bad_ups) {

                        long t = bad.getWhen();

                        if (real_now - t <= 30 * 1000 && bad.getBytesPerSec() != last_bad_limit) {

                            total += bad.getBytesPerSec();

                            count++;
                        }
                    }

                    latest_choke = total / count;

                    int new_rate_limit;

                    if (rate_limit == 0) {

                        new_rate_limit = latest_choke / 2;

                    } else {

                        new_rate_limit = rate_limit / 2;
                    }

                    if (new_rate_limit < slack_bytes_per_sec) {

                        new_rate_limit = slack_bytes_per_sec;
                    }

                    rate_limit = new_rate_limit;

                    wait_until_tick = tick_count + WAIT_AFTER_CHOKE_TICKS;

                    ticks_to_sample_start = 0;
                    sample_num = 0;
                    complete_samples = 0;
                    incomplete_samples = 0;
                    last_rate_limit = 0;

                    return;
                }
            }

            if (ticks_to_sample_start > 0) {

                ticks_to_sample_start--;

            } else if (sample_num < SAMPLE_COUNT) {

                complete_samples += c_up_total;
                incomplete_samples += i_up_total;

                sample_num++;

            } else {

                double incomplete_average = incomplete_samples / SAMPLE_COUNT;
                double complete_average = complete_samples / SAMPLE_COUNT;
                double overall_average = (complete_samples + incomplete_samples) / SAMPLE_COUNT;

                int action = -1;

                try {

                    if (last_rate_limit == 0) {

                        action = 1;

                    } else {

                        double overall_change = overall_average - last_overall_average;

                        if (overall_change < 0) {

                            if (rate_limit < last_rate_limit) {

                                // System.out.println( "average decreased" );

                                action = 1;

                            } else {

                                action = 0;
                            }
                        } else {

                            double last_ratio = last_incomplete_average / last_complete_average;
                            double ratio = incomplete_average / complete_average;

                            // System.out.println( "rate=" + rate_limit + "/" + last_rate_limit + ", ratio=" + ratio + "/" + last_ratio );

                            if (rate_limit < last_rate_limit && ratio >= last_ratio) {

                                action = -1;

                            } else if (rate_limit > last_rate_limit && ratio <= last_ratio) {

                                double i_up_change = incomplete_average - last_incomplete_average;

                                if (i_up_change >= 1024) {

                                    action = -1;

                                } else {

                                    action = 1;
                                }

                            } else {

                                action = 1;
                            }
                        }
                    }

                } finally {

                    int new_rate_limit;

                    if (action > 0) {

                        int ceiling = latest_choke == 0 ? DEFAULT_UP_LIMIT : latest_choke;

                        int diff = (ceiling - rate_limit) / 4;

                        if (diff > MAX_UP_DIFF) {

                            diff = MAX_UP_DIFF;

                        } else if (diff < MIN_DIFF) {

                            diff = MIN_DIFF;
                        }

                        new_rate_limit = rate_limit + diff;

                        if (new_rate_limit > 100 * 1024 * 1024) {

                            new_rate_limit = 100 * 1024 * 1024;
                        }
                    } else if (action < 0) {

                        int diff = rate_limit / 5;

                        if (diff > MAX_DOWN_DIFF) {

                            diff = MAX_DOWN_DIFF;

                        } else if (diff < MIN_DIFF) {

                            diff = MIN_DIFF;
                        }

                        new_rate_limit = rate_limit - diff;

                        if (new_rate_limit < slack_bytes_per_sec) {

                            new_rate_limit = slack_bytes_per_sec;
                        }
                    } else {

                        new_rate_limit = rate_limit;
                    }

                    last_rate_limit = rate_limit;
                    last_overall_average = overall_average;
                    last_complete_average = complete_average;
                    last_incomplete_average = incomplete_average;

                    rate_limit = new_rate_limit;

                    sample_num = 0;
                    complete_samples = 0;
                    incomplete_samples = 0;
                }
            }

        } finally {

            // System.out.println( "rate=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( rate_limit ) + ", last_choke=" + latest_choke );
        }
    }

    private static class PMState {
        final private PEPeerManager manager;

        private boolean complete;
        private long bytes_up;

        private boolean interesting;
        private long last_interesting_calc;

        private PMState(PEPeerManager _manager, boolean _complete, long _bytes_up) {
            manager = _manager;
            complete = _complete;
            bytes_up = _bytes_up;
        }

        private boolean isComplete() {
            return (complete);
        }

        private void setComplete(boolean c) {
            complete = c;
        }

        private long setBytesUp(long b) {
            long diff = b - bytes_up;

            bytes_up = b;

            return (diff);
        }

        private boolean isInteresting(long now) {
            boolean calc;

            if (last_interesting_calc == 0) {

                calc = true;

            } else if (!interesting) {

                calc = now - last_interesting_calc >= 5 * 1000;

            } else {

                calc = now - last_interesting_calc >= 60 * 1000;
            }

            if (calc) {

                last_interesting_calc = now;

                PEPeerManagerStats stats = manager.getStats();

                // not interesting if stalled downloading

                long dl_rate = stats.getDataReceiveRate();

                if (dl_rate < 5 * 1024) {

                    interesting = false;

                } else {

                    // not interesting if we have nobody to seed to!

                    if (manager.getNbPeersUnchoked() < 3) {

                        interesting = false;

                    } else {

                        // see if the download has a manually imposed upload limit and if
                        // we are close to it

                        int limit = manager.getUploadRateLimitBytesPerSecond();

                        if (limit > 0 && (stats.getDataSendRate() + stats.getProtocolSendRate()) >= (limit - (5 * 1024))) {

                            interesting = false;

                        } else {

                            interesting = true;
                        }
                    }
                }
            }

            return (interesting);
        }
    }
}
